Chapter 16

Performance and Profiling

Session 16

Learning Objectives

By the end of this chapter, you will be able to:

1

Measurement First

Always measure before optimizing; guessing leads to wasted effort.

Measurement Process

  • Reproduce a problematic scenario reliably (sample data, device/emulator, orientation).
  • Collect baseline metrics: frame times (jank), memory usage, CPU load, and network timings.

Tools

  • Flutter DevTools (performance, timeline, memory, CPU profiler).
  • Observatory and Dart DevTools for allocations and GC.
  • Platform profilers (Android Studio / Xcode Instruments) for native layers.
2

Rendering and Frame Budget

Understanding frame budgets is crucial for smooth animations.

Frame Budget Targets

  • Target: 16ms per frame for 60 FPS, ~8ms for 120 FPS.
  • Identify jank with the timeline: look for long frames, raster vs UI thread hotspots, and UI thread stalls.
  • Common causes: expensive work in build(), heavy layouts (Intrinsic*), long synchronous loops, synchronous I/O on UI thread, and large image decoding in paint.

Strategies

  • Move heavy computation off the UI thread (compute, isolates, or native services).
  • Avoid expensive layout ops such as IntrinsicWidth/Height, nested ListViews without builders, and deep widget rebuild churn.
  • Use const constructors and small widgets to reduce rebuild cost.

Quick Checks

  • Wrap suspect subtree with RepaintBoundary to isolate rasterization when appropriate.
  • Use Profile mode to get realistic engine timings; release mode for final validation.
3

Reducing Rebuilds and Widget Churn

Minimizing rebuilds is key to performance.

Best Practices

  • Scope state narrowly: limit setState to the smallest meaningful subtree and lift state only when needed.
  • Use Provider/Selector, context.select, or ValueListenableBuilder to rebuild only dependents.
  • Extract frequently rebuilt subtrees into separate widgets (prefer const where possible).
  • Avoid rebuilding lists entirely when a single item changes; use keys and item-level notifiers.

Patterns

  • Use const constructors for static widgets.
  • Use AnimatedBuilder, ListenableBuilder, or StreamBuilder to update only the changing parts.
  • Use shouldRebuild and didUpdateWidget checks in custom RenderObject/Sliver delegates.

Example: Use Selector

final count = context.select((m) => m.items.length);
4

Layout and Paint Optimizations

Optimizing layout and paint operations improves rendering performance.

Layout Optimization

  • Minimize layout passes: prefer simpler widgets (Padding, SizedBox) instead of nesting many Containers.
  • Avoid Intrinsic widgets and excessive use of Align or FractionallySizedBox in deep hierarchies.
  • Cache complex paints: use RepaintBoundary to separate repaint layers and reduce repaint area.
  • Use GPU-friendly transitions (Opacity with AnimatedOpacity uses compositing instead of repainting large subtrees).

Image Handling

  • Use resized images that match target display size; avoid loading huge images then scaling down.
  • Use cached_network_image or precacheImage and provide lower-resolution thumbnails for lists.
  • Use filterQuality: FilterQuality.low for thumbnails and high for detail views where needed.

Text and Fonts

  • Use font subsets and avoid excessive custom fonts.
  • Reuse TextStyle from Theme and avoid repeated TextStyle.copyWith in hot paths.
5

Memory and GC Management

Efficient memory management prevents jank and crashes.

Memory Best Practices

  • Monitor allocation patterns: frequent short-lived allocations increase GC pressure and jank.
  • Avoid creating many temporary objects inside build() (especially loops creating widgets/data).
  • Reuse controllers and objects where lifecycle allows; dispose controllers in dispose().
  • Use const objects and compile-time constants to reduce allocation.

Leak Detection

  • Use DevTools memory profiler to track retained objects and inspect allocation traces.
  • Check for listeners not removed, un-disposed controllers, and large caches retained unexpectedly.

Image Memory

Decode images at an appropriate size and evict large cached images when memory pressure grows.

6

Lists, Grids, and Large Data Sets

Optimizing list rendering is critical for performance.

List Optimization

  • Use builder constructors (ListView.builder, GridView.builder) and Slivers to lazily build items.
  • Use itemExtent or prototypeItem when items are same height to avoid layout cost.
  • Implement pagination/cursor loading and show placeholders; avoid loading thousands of items in memory.
  • Use ReorderableListView, Dismissible, and other interactive widgets with keys to preserve state efficiently.

Best practice: For large lists, supply itemExtent or use SliverFixedExtentList to improve layout performance.

7

Network and I/O Optimizations

Efficient network and I/O operations improve perceived performance.

Network Optimization

  • Batch and debounce network calls (search, scroll-triggered loads).
  • Use HTTP caching headers and client-side caching with expiry to reduce redundant traffic.
  • Use background isolates or compute() for expensive JSON parsing if it blocks UI for large payloads.
  • Stream large downloads and parse progressively when possible.

Profiling Network

  • Use Charles/Fiddler or platform network profilers to measure latency, payload sizes, and compression.
  • Measure cold vs warmed caches and optimize caching strategy for first-run UX.
8

Async Work and Isolates

Offloading heavy work to isolates keeps the UI responsive.

Isolate Usage

  • Offload CPU-heavy tasks (image processing, large JSON decoding, heavy calculations) to isolates using compute() or manual Isolate spawn.
  • Be mindful of serialization costs between isolates; keep payloads minimal and use transferable data when possible.
  • Use native plugins or platform channels for extremely heavy work that benefits from native libraries.

Example Isolate Usage

final parsed = await compute(parseLargeJson, rawString);
9

Flutter-Specific Gotchas and Fixes

Avoiding common pitfalls improves performance.

Common Pitfalls

  • Avoid calling setState inside build or within synchronous loops.
  • Don't use WidgetsBinding.instance.addPostFrameCallback for repeated work; schedule only when necessary.
  • Avoid large rebuilds caused by changing parent widget keys or using UniqueKey indiscriminately.
  • Beware of ListView inside Column without constraints causing expensive layout or overflow; prefer Expanded.
10

Instrumentation and Regression Testing

Automated performance testing prevents regressions.

Performance Testing

  • Integrate performance tests into CI for critical flows (e.g., scrolling a list of N items under X ms).
  • Capture baseline traces and compare after changes; automate trace collection where possible.
  • Use synthetic workload in integration tests to measure frame timings and assert no regressions.
11

Practical Optimization Checklist

Follow this systematic approach to optimization.

  1. Measure and reproduce the problem.
  2. Profile in profile/release modes.
  3. Apply targeted fixes (narrow state, avoid heavy widgets, cache images).
  4. Verify with DevTools timeline and memory snapshots.
  5. Add regression tests if possible and document the change.
12

Exercises

Practice what you've learned with these exercises:

1. Profile a sample app with a slow-scrolling list

Identify the top 3 hotspots from the timeline, and implement fixes (e.g., itemExtent, caching images, splitting widgets). Provide before/after traces and a short report.

2. Create a compute() demo

Parse a large JSON on a background isolate and measure UI responsiveness improvement.

3. Build a stress test

Simulate rapid navigation and list loading to surface memory leaks; fix a discovered leak and demonstrate with memory snapshots.